package indigo.mybatis.entity;

import static com.google.common.base.CaseFormat.LOWER_CAMEL;
import static com.google.common.base.CaseFormat.LOWER_UNDERSCORE;
import static com.google.common.base.CaseFormat.UPPER_CAMEL;

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Map;

import org.apache.ibatis.reflection.DefaultReflectorFactory;
import org.apache.ibatis.reflection.MetaClass;
import org.apache.ibatis.reflection.ReflectorFactory;
import org.apache.ibatis.reflection.invoker.Invoker;
import org.apache.ibatis.reflection.invoker.MethodInvoker;

import com.google.common.collect.Maps;

import indigo.mybatis.entity.annotation.ResultCache;
import indigo.mybatis.entity.annotation.ResultCacheRef;
import indigo.mybatis.entity.annotation.ResultEntity;
import indigo.mybatis.entity.annotation.ResultId;
import indigo.mybatis.entity.annotation.ResultProperty;
import indigo.mybatis.entity.annotation.ResultPropertyIgnore;

public class AnnotationEntityMetadataReader implements EntityMetadataReader {
	private static final Field methodInvokerMethodField;
	static {
		try {
			methodInvokerMethodField = MethodInvoker.class.getDeclaredField("method");
			methodInvokerMethodField.setAccessible(true);
		} catch (NoSuchFieldException e) {
			throw new IllegalStateException("No field 'method' found in MethodInvoker");
		}
	}

	private final ReflectorFactory reflectorFactory;

	public AnnotationEntityMetadataReader() {
		this(new DefaultReflectorFactory());
	}

	public AnnotationEntityMetadataReader(ReflectorFactory reflectorFactory) {
		this.reflectorFactory = reflectorFactory;
	}

	protected Field findPropertyField(Class<?> type, String propertyName) {
		Field field = null;
		if (!type.isInterface()) {
			try {
				field = type.getDeclaredField(propertyName);
			} catch (NoSuchFieldException e) {
				Class<?> superType = type.getSuperclass();
				if (superType != Object.class) {
					return findPropertyField(superType, propertyName);
				}
			}
		}
		if (field != null) {
			int modifiers = field.getModifiers();
			if (!Modifier.isStatic(modifiers) && !Modifier.isPublic(modifiers)) {
				field.setAccessible(true);
			}
		}
		return field;
	}

	protected Method getInvokerMethod(Invoker invoker) {
		if (invoker instanceof MethodInvoker) {
			try {
				return (Method) methodInvokerMethodField.get(invoker);
			} catch (IllegalAccessException ignored) {
			}
		}
		return null;
	}

	protected <A extends Annotation> A findAnnotation(Class<A> annotationType, AnnotatedElement... elements) {
		for (AnnotatedElement element : elements) {
			if (element != null) {
				A annotation = element.getDeclaredAnnotation(annotationType);
				if (annotation != null) {
					return annotation;
				}
			}
		}
		return null;
	}

	protected void resolveEntity(EntityMetadata.Builder<?> builder) {
		Class<?> type = builder.getType();
		ResultEntity annEntity = type.getDeclaredAnnotation(ResultEntity.class);
		if (annEntity != null) {
			builder.setName(annEntity.name());
			builder.setTableName(annEntity.tableName());
		}
		if (builder.getName() == null) {
			builder.setName(UPPER_CAMEL.to(LOWER_CAMEL, type.getSimpleName()));
		}
		if (builder.getTableName() == null) {
			builder.setTableName(UPPER_CAMEL.to(LOWER_UNDERSCORE, type.getSimpleName()));
		}
	}

	protected void resolveCacheNamespaceRef(EntityMetadata.Builder<?> builder) {
		ResultCacheRef an = builder.getType().getDeclaredAnnotation(ResultCacheRef.class);
		if (an != null) {
			String namespace = an.namespace();
			Class<?> entity = an.entity();
			if (!namespace.isEmpty() && entity != Object.class) {
				throw new IllegalStateException("namespace and entity cannot be set together");
			}
			if (namespace.isEmpty() && entity == Object.class) {
				throw new IllegalStateException("Neither namespace nor entity are set");
			}
			builder.setCacheNamespaceRef(!namespace.isEmpty() ? namespace : entity.getName());
		}
	}

	protected void resolveCache(EntityMetadata.Builder<?> builder) {
		ResultCache an = builder.getType().getDeclaredAnnotation(ResultCache.class);
		if (an != null) {
			CacheMetadata.Builder cacheBuilder = new CacheMetadata.Builder();
			cacheBuilder.setImplementation(an.implementation());
			cacheBuilder.setEviction(an.eviction());
			cacheBuilder.setReadWrite(an.readWrite());
			cacheBuilder.setBlocking(an.blocking());
			cacheBuilder.setFlushInterval(an.flushInterval() > 0 ? an.flushInterval() : null);
			cacheBuilder.setSize(an.size() > 0 ? an.size() : null);

			Map<String, String> properties = Maps.newHashMap();
			for (ResultCache.Property property : an.properties()) {
				properties.put(property.name(), property.value());
			}
			cacheBuilder.setProperties(properties);

			builder.setCache(cacheBuilder.build());
		}
	}

	protected void resolveProperties(EntityMetadata.Builder<?> builder) {
		Class<?> type = builder.getType();
		MetaClass metaType = MetaClass.forClass(type, reflectorFactory);
		Map<String, PropertyMetadata> props = Maps.newHashMap();
		for (String propName : metaType.getGetterNames()) {
			Method getter = getInvokerMethod(metaType.getGetInvoker(propName));
			Method setter = getInvokerMethod(metaType.getSetInvoker(propName));
			Field field = findPropertyField(builder.getType(), propName);
			AnnotatedElement[] elements = { field, getter, setter };

			if (findAnnotation(ResultPropertyIgnore.class, elements) != null) {
				continue;
			}

			PropertyMetadata.Builder propBuilder = new PropertyMetadata.Builder();
			propBuilder.setName(propName);
			propBuilder.setType(metaType.getGetterType(propName));
			propBuilder.setField(field);
			propBuilder.setGetter(getter);
			propBuilder.setSetter(setter);

			ResultProperty an = findAnnotation(ResultProperty.class, elements);
			if (an != null) {
				propBuilder.setColumnName(an.columnName());
				propBuilder.setJdbcType(an.jdbcType());
				propBuilder.setInsertable(an.insertable());
				propBuilder.setUpdatable(an.updatable());
			}
			if (propBuilder.getColumnName() == null) {
				propBuilder.setColumnName(LOWER_CAMEL.to(LOWER_UNDERSCORE, propName));
			}

			if (findAnnotation(ResultId.class, elements) != null) {
				if (builder.getIdPropertyName() != null) {
					throw new IllegalStateException("Multiple ID properties found");
				}
				builder.setIdPropertyName(propName);
			}

			props.put(propName, propBuilder.build());
		}
		if (builder.getIdPropertyName() == null) {
			throw new IllegalStateException("No ID property found");
		}
		builder.setProperties(props);
	}

	@Override
	public <T> EntityMetadata<T> getEntityMetadata(Class<T> type) {
		EntityMetadata.Builder<T> builder = new EntityMetadata.Builder<>(type);
		resolveEntity(builder);
		resolveCacheNamespaceRef(builder);
		resolveCache(builder);
		resolveProperties(builder);

		return builder.build();
	}
}
